Skip to content

feat: KnowledgeBase Enhancements + Knowledge Ingestion flow + Polymorphic Job Tracking - #11541

Merged
deon-sanchez merged 127 commits into
mainfrom
le-207
Feb 27, 2026
Merged

feat: KnowledgeBase Enhancements + Knowledge Ingestion flow + Polymorphic Job Tracking#11541
deon-sanchez merged 127 commits into
mainfrom
le-207

Conversation

@deon-sanchez

@deon-sanchez deon-sanchez commented Feb 2, 2026

Copy link
Copy Markdown
Collaborator

LE-207

Frontend Work:

@deon-sanchez

  • New Features
    • Enabled Knowledge Bases feature
    • Create Knowledge action now displays File and Folder options via dropdown menu
    • Knowledge bases table redesigned with reorganized columns: Source (now sortable), Type, Owner, and Avg Chunk Size
    • Added Status column to knowledge bases display

Testing Steps for QA

KNOWLEDGE BASES FEATURE — QA TEST PLAN.pdf

Backend Work:

@dkaushik94

Description

This PR introduces a significant refactor of the Knowledge Bases infrastructure alongside generalizations to our Job/Task service and backend safety protocols. The implementations resolve existing data contention issues while enabling cleaner background task handling and paginated results.

🚀 Features & Enhancements

  • Knowledge Base Architecture & Storage Refactor

    • Class-based Helpers: Abstracted standalone functions into isolated, responsibility-driven classes (KBStorageHelper, KBAnalysisHelper, KBIngestionHelper) to significantly improve code modularity and testability.
    • Chunk Pagination & Filter APIs: Integrated pagination frameworks and text-match filtering into get_chunks endpoints to streamline handling large vector retrieval natively.
    • Asynchronous Ingestion: Moved core file data chunking and embedding logic into non-blocking TaskService deployments with tracking managed by internal JobService. Supports both status polling and dynamic job cancellation.
    • Chroma/SQLite Contention Fixes: Addressed historical Read-Only exceptions and lock contention by generating a forced fresh Chroma persistent client prior to active storage allocation and ensuring resources are properly garbage-collected upon teardowns.
    • Improved Metadata Tracking: Enforces proper tracking of data types like avg_chunk_size, words, and source_types, effectively caching them locally to speed up /knowledge_bases/ fetching loops by drastically omitting repetitive folder iterations.
  • Job and Task Service Generalizations

    • Modified background jobs schemas via Alembic migrations. Included job_type, asset_id, and asset_type columns to allow polymorphism. This grants jobs the elasticity to process Canvas evaluations, Datasets mapping, and KB ingestions agnostically without blocking primary router loops.

🛠️ Refactoring & Code Quality

  • Security & Error Handling: Refactored internal HTTPExceptions. Raw exception objects are no longer leaked directly to users; issues are safely logged locally (logger.aerror()) and users only receive controlled standard text formats globally, addressing blind exception catching.
  • Format Alignments: Stripped inline f-string formats strictly to comply with ruff format UP031 and corrected file-length guidelines.
  • Testing Expansion: Augmented test_knowledge_bases_api vastly to fully test edge-case states (zero-file uploads, missing dimensions, missing metadata recovery, ingestion rollback).

🐛 Additional Fixes

  • Fixed _get_text_columns fallback bypass which previously forced avg_chunk_size calculations to universally output 0.0.
  • Refreshed base starter project structure files.

@coderabbitai

coderabbitai Bot commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Enable knowledge bases feature flag. Replace knowledge creation button with dropdown menu offering File/Folder options. Restructure knowledge base columns: rename Name to Source (sortable), remove embedding model column, replace Words/Characters columns with Type/Owner, rename Avg Chunks to Avg Chunk Size, add Status column.

Changes

Cohort / File(s) Summary
Feature Flag
src/frontend/src/customization/feature-flags.ts
Toggled ENABLE_KNOWLEDGE_BASES from false to true to activate knowledge bases functionality.
UI Component
src/frontend/src/pages/MainPage/pages/filesPage/components/KnowledgeBasesTab.tsx
Replaced standalone Create Knowledge button with DropdownMenu component containing File and Folder options, both routing through existing handleCreateKnowledge function.
Table Configuration
src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx
Restructured knowledge base table columns: renamed "Name" to "Source" (now sortable), replaced "Words" with "Type", replaced "Characters" with "Owner", renamed "Avg Chunks" to "Avg Chunk Size", added new "Status" column, removed embedding model column.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 4

❌ Failed checks (1 error, 2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error Test file exists but is a placeholder mock that does not test the new DropdownMenu component, File/Folder menu items, handleCreateKnowledge function, knowledgeBaseColumns changes, or feature-flags modifications. Replace mock test with real integration test rendering actual KnowledgeBasesTab component, verify DropdownMenu rendering, test File/Folder menu item behaviors, add knowledgeBaseColumns.tsx test file, and verify ENABLE_KNOWLEDGE_BASES flag usage.
Test Quality And Coverage ⚠️ Warning Pull request lacks comprehensive test coverage for critical UI changes including dropdown menu, column configuration, and feature flag behavior. Update frontend tests to remove component mock and test actual KnowledgeBasesTab implementation with dropdown menu rendering, click handlers, async functions, new columns, and feature flag toggle.
Test File Naming And Structure ⚠️ Warning PR introduces significant UI changes without adequate test coverage for dropdown menu functionality, column reconfigurations, and feature flag changes. Add comprehensive tests for dropdown menu interactions, create test file for knowledgeBaseColumns.tsx, add tests for feature flag change, and ensure dropdown handlers differentiate between File and Folder options.
Title check ❓ Inconclusive The PR title mentions 'KnowledgeBase Enhancements' and 'Knowledge Ingestion flow' but the changeset shows only feature flag enablement and UI updates to knowledge base components, not the full scope implied. Consider whether the title accurately represents these incremental UI/feature flag changes, or if it should be more specific to the actual modifications (e.g., 'feat: Add dropdown menu to knowledge base creation UI and enable knowledge bases feature flag').
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Excessive Mock Usage Warning ✅ Passed The pull request does not include any test file modifications, only feature implementation changes to frontend source files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch le-207

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@src/frontend/src/pages/MainPage/pages/filesPage/components/KnowledgeBasesTab.tsx`:
- Around line 200-205: The two DropdownMenuItem entries both call
handleCreateKnowledge with no distinction; update the calls to pass an explicit
type (e.g., 'file' and 'folder') and modify the handleCreateKnowledge function
signature to accept that parameter and branch on it to choose the correct
creation flow/template (or, if placeholder behavior is intended, add a clear
TODO comment next to both DropdownMenuItem entries indicating that
differentiation is pending). Ensure any TypeScript types for
handleCreateKnowledge (and any callers) are updated accordingly and that
branching uses the passed value to select the proper "Knowledge Ingestion"
example or a folder creation template.

In
`@src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx`:
- Around line 33-48: The grid columns in knowledgeBaseColumns.tsx reference
params.data.type, params.data.owner, and params.data.status but those properties
are missing from the KnowledgeBaseInfo interface; update the KnowledgeBaseInfo
interface in use-get-knowledge-bases.ts to include type, owner, and status with
appropriate types (or, if the backend doesn't supply them, remove the
corresponding columns from knowledgeBaseColumns.tsx or map existing API fields
into these properties before passing rows to the grid) so the valueGetters
return real data instead of the placeholder "—".

Comment on lines +200 to +205
<DropdownMenuItem onClick={handleCreateKnowledge}>
File
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCreateKnowledge}>
Folder
</DropdownMenuItem>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Both "File" and "Folder" options invoke the same handler with no differentiation.

Both DropdownMenuItem elements call handleCreateKnowledge without any parameter to distinguish between File and Folder creation. The underlying handleCreateKnowledge function always uses the same "Knowledge Ingestion" example template regardless of which option is selected.

If this is intentional placeholder behavior, consider adding a TODO comment. Otherwise, pass a parameter to differentiate the action:

Suggested approach
-const handleCreateKnowledge = async () => {
+const handleCreateKnowledge = async (sourceType: "file" | "folder") => {
   const knowledgeBasesExample = examples.find(
     (example) => example.name === "Knowledge Ingestion",
   );
+  // TODO: Use sourceType to configure the flow appropriately
-            <DropdownMenuItem onClick={handleCreateKnowledge}>
+            <DropdownMenuItem onClick={() => handleCreateKnowledge("file")}>
               File
             </DropdownMenuItem>
-            <DropdownMenuItem onClick={handleCreateKnowledge}>
+            <DropdownMenuItem onClick={() => handleCreateKnowledge("folder")}>
               Folder
             </DropdownMenuItem>
🤖 Prompt for AI Agents
In
`@src/frontend/src/pages/MainPage/pages/filesPage/components/KnowledgeBasesTab.tsx`
around lines 200 - 205, The two DropdownMenuItem entries both call
handleCreateKnowledge with no distinction; update the calls to pass an explicit
type (e.g., 'file' and 'folder') and modify the handleCreateKnowledge function
signature to accept that parameter and branch on it to choose the correct
creation flow/template (or, if placeholder behavior is intended, add a clear
TODO comment next to both DropdownMenuItem entries indicating that
differentiation is pending). Ensure any TypeScript types for
handleCreateKnowledge (and any callers) are updated accordingly and that
branching uses the passed value to select the proper "Knowledge Ingestion"
example or a folder creation template.

Comment on lines +33 to +48
headerName: "Type",
field: "type",
flex: 1,
sortable: false,
editable: false,
cellClass: baseCellClass,
valueFormatter: (params) => formatNumber(params.value),
valueGetter: (params) => params.data.type || "—",
},
{
headerName: "Characters",
field: "characters",
headerName: "Owner",
field: "owner",
flex: 1,
sortable: false,
editable: false,
cellClass: baseCellClass,
valueFormatter: (params) => formatNumber(params.value),
valueGetter: (params) => params.data.owner || "—",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that type, owner, and status fields exist on KnowledgeBaseInfo interface

echo "=== Searching for KnowledgeBaseInfo type definition ==="
rg -n 'type KnowledgeBaseInfo|interface KnowledgeBaseInfo' --type ts -A 20

echo ""
echo "=== Check the knowledgeBaseColumns file itself for imports ==="
head -30 src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx

echo ""
echo "=== Search for KnowledgeBaseInfo in the entire codebase ==="
rg -l 'KnowledgeBaseInfo' --type ts

Repository: langflow-ai/langflow

Length of output: 6672


🏁 Script executed:

#!/bin/bash
# Read the full knowledgeBaseColumns.tsx file to verify all field references
echo "=== Full knowledgeBaseColumns.tsx file ==="
cat -n src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx

echo ""
echo "=== Full KnowledgeBaseInfo interface ==="
cat -n src/frontend/src/controllers/API/queries/knowledge-bases/use-get-knowledge-bases.ts | head -30

Repository: langflow-ai/langflow

Length of output: 3774


Add type, owner, and status fields to the KnowledgeBaseInfo interface or remove these columns.

The columns at lines 33-49 and 68-76 reference params.data.type, params.data.owner, and params.data.status, but these fields do not exist on the KnowledgeBaseInfo interface defined in src/frontend/src/controllers/API/queries/knowledge-bases/use-get-knowledge-bases.ts. This will cause these columns to always display the placeholder "—". Either extend the interface and ensure the API returns these fields, or remove these columns from the grid.

🤖 Prompt for AI Agents
In
`@src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx`
around lines 33 - 48, The grid columns in knowledgeBaseColumns.tsx reference
params.data.type, params.data.owner, and params.data.status but those properties
are missing from the KnowledgeBaseInfo interface; update the KnowledgeBaseInfo
interface in use-get-knowledge-bases.ts to include type, owner, and status with
appropriate types (or, if the backend doesn't supply them, remove the
corresponding columns from knowledgeBaseColumns.tsx or map existing API fields
into these properties before passing rows to the grid) so the valueGetters
return real data instead of the placeholder "—".

@github-actions github-actions Bot added the enhancement New feature or request label Feb 3, 2026
@github-actions

github-actions Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 22%
22.04% (7566/34320) 14.69% (3947/26855) 14.88% (1081/7261)

Unit Test Results

Tests Skipped Failures Errors Time
2507 0 💤 0 ❌ 0 🔥 41.082s ⏱️

@codecov

codecov Bot commented Feb 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.58333% with 205 lines in your changes missing coverage. Please review.
✅ Project coverage is 34.21%. Comparing base (6ed0091) to head (a99ee57).

Files with missing lines Patch % Lines
...rc/backend/base/langflow/api/v1/knowledge_bases.py 18.11% 113 Missing ⚠️
...age/pages/knowledgePage/sourceChunksPage/index.tsx 0.00% 46 Missing ⚠️
...eries/knowledge-bases/use-create-knowledge-base.ts 0.00% 14 Missing ⚠️
...s/knowledge-bases/use-get-knowledge-base-chunks.ts 0.00% 14 Missing ⚠️
...nd/src/components/core/dropdownComponent/index.tsx 0.00% 10 Missing ⚠️
...Component/components/modelInputComponent/index.tsx 20.00% 0 Missing and 4 partials ⚠️
...d/src/pages/MainPage/pages/knowledgePage/index.tsx 0.00% 3 Missing ⚠️
src/frontend/src/routes.tsx 0.00% 1 Missing ⚠️

❌ Your project check has failed because the head coverage (49.51%) is below the target coverage (55.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #11541      +/-   ##
==========================================
- Coverage   35.20%   34.21%   -1.00%     
==========================================
  Files        1521     1453      -68     
  Lines       72923    69890    -3033     
  Branches    10936    10051     -885     
==========================================
- Hits        25674    23911    -1763     
+ Misses      45854    44730    -1124     
+ Partials     1395     1249     -146     
Flag Coverage Δ
backend 49.51% <18.11%> (-6.16%) ⬇️
lfx 42.10% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...enderComponent/components/tableComponent/index.tsx 0.00% <ø> (ø)
...eries/knowledge-bases/use-delete-knowledge-base.ts 0.00% <ø> (ø)
src/frontend/src/customization/feature-flags.ts 100.00% <100.00%> (ø)
...d/src/modals/baseModal/helpers/switch-case-size.ts 14.81% <ø> (+7.87%) ⬆️
...tend/src/modals/knowledgeBaseUploadModal/index.tsx 100.00% <100.00%> (ø)
...ntend/src/pages/MainPage/pages/filesPage/index.tsx 0.00% <ø> (ø)
src/lfx/src/lfx/custom/validate.py 40.27% <100.00%> (ø)
src/frontend/src/routes.tsx 0.00% <0.00%> (ø)
...d/src/pages/MainPage/pages/knowledgePage/index.tsx 0.00% <0.00%> (ø)
...Component/components/modelInputComponent/index.tsx 70.28% <20.00%> (+0.22%) ⬆️
... and 5 more

... and 249 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@deon-sanchez deon-sanchez changed the title feat: Add native knowledge base ingestion and retrieval for RAG feat: KB updates and implementations Feb 3, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Feb 3, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Feb 3, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Feb 3, 2026
…mprove UI layout

- Remove .title() transformation from knowledge base names in API endpoints
- Add textTransform: none to knowledge base name column in grid
- Improve source chunks page layout with proper overflow handling
- Enhance chunk card UI with badges, better spacing, and copy feedback
- Add pagination controls with first/last page buttons and page number input
- Preserve original chunk indices when filtering
- Fix whit
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Feb 3, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Feb 4, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Feb 4, 2026
Comment on lines +216 to +217
total_characters += int(text_series.str.len().sum())
total_words += int(text_series.str.split().str.len().sum())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡️Codeflash found 215% (2.15x) speedup for calculate_text_metrics in src/backend/base/langflow/api/utils/kb_helpers.py

⏱️ Runtime : 14.1 milliseconds 4.47 milliseconds (best of 286 runs)

📝 Explanation and details

Brief: The optimized version removes expensive pandas string-accessor work and large temporary pandas objects by converting the series to a plain NumPy array of Python strings and doing the len()/split() work in tight Python loops. That cuts pandas overhead and allocations, reducing runtime from 14.1 ms to 4.47 ms (~3.15× faster; reported 214% speedup).

What changed

  • Replaced two pandas .str operations per column (text_series.str.len().sum() and text_series.str.split().str.len().sum()) with:
    • text_series.to_numpy() to get an ndarray of Python strings, and
    • generator expressions sum(len(s) for s in arr) and sum(len(s.split()) for s in arr).
  • Kept astype(str).fillna("") to preserve semantics (None/nan become the literal strings), so behavior remains unchanged.

Why this is faster

  • Pandas .str accessor allocates intermediate Series/arrays and does nontrivial bookkeeping for each vectorized call. In the original code each column triggered multiple .str operations (multiple passes and temporary structures), which the profiler shows dominated runtime (large percentages on .str.len() and .str.split()).
  • Converting to a NumPy array once (to_numpy()) avoids per-operation pandas overhead. Iterating Python strings with built-in len() and str.split() is very cheap compared with the cost of allocating and manipulating pandas Series objects and the lists produced by .str.split().
  • Reduced object churn: .str.split() would produce Python lists or Series-of-lists, causing extra allocations. The optimized code performs the splits and length calculations in-place on the existing strings, avoiding those intermediate allocations.
  • The profiler confirms this: the heavy lines in the original (.str operations) shrink substantially in the optimized run, while the cheaper Python-level loops take a small fraction of time.

Behavior and trade-offs

  • Behavior is preserved: astype(str).fillna("") is still used, so None/nan/string handling remains the same and all tests pass.
  • Memory: to_numpy() creates an ndarray of object references (not duplicating the full string contents), which is a small allocation compared to the savings from avoiding large temporary Series objects. For very small DataFrames, the difference is negligible; for larger ones (seen in annotated tests like 1000 rows) the benefit grows.
  • Remaining hotspot: astype(str).fillna("") still shows as a significant cost in the profiler; if further speedup is required, you can explore bulk conversion strategies (e.g., processing subsets, avoiding unnecessary conversions, or using a single df[text_columns].astype(str) call) but that’s outside this change’s scope.

When this optimization helps most

  • Dataframes with many rows and large text columns (the "large_scale_1000_rows" and similar annotated tests) — these show the biggest wins because they eliminate repeated pandas allocations and vectorized-access overhead.
  • Little benefit for tiny data where pandas overhead is already small relative to total runtime.

Summary

  • Key win: avoid repeated pandas .str accessor work and temporary Series/list allocations by doing a single to_numpy() + efficient Python iteration per column.
  • Result: same behavior, substantially lower runtime (14.1 ms → 4.47 ms measured), and much lower per-column overhead in realistic, row-heavy workloads.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 12 Passed
🌀 Generated Regression Tests 18 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
⚙️ Click to see Existing Unit Tests
🌀 Click to see Generated Regression Tests
import pandas as pd  # used to construct DataFrame instances
# imports
import pytest  # used for our unit tests
from langflow.api.utils.kb_helpers import calculate_text_metrics


def test_empty_dataframe_returns_zero():
    # An entirely empty DataFrame with no columns should produce zero counts.
    df = pd.DataFrame()  # real DataFrame instance with no columns and no rows
    total_words, total_chars = calculate_text_metrics(df, ["any_column"])


def test_basic_single_column_simple():
    # Basic functionality: single text column with typical strings.
    df = pd.DataFrame(
        {
            "text": [
                "hello world",  # 2 words, 11 characters (includes the space)
                "test",         # 1 word, 4 characters
                "",             # 0 words, 0 characters
            ]
        }
    )
    # Only the 'text' column is measured
    words, chars = calculate_text_metrics(df, ["text"])


def test_multiple_columns_and_missing_column():
    # Verify function handles multiple text columns and skips missing ones.
    df = pd.DataFrame(
        {
            "a": ["x y", "z"],  # 'x y' -> 2 words, 3 chars; 'z' -> 1 word, 1 char
            "b": [100, 200],    # numeric values will be astyped to '100', '200'
        }
    )
    # Include a non-existent column name 'missing' which should be ignored.
    words, chars = calculate_text_metrics(df, ["a", "missing", "b"])


def test_text_columns_empty_list_returns_zero_even_with_data():
    # If text_columns is empty, no columns are processed even if DataFrame has data.
    df = pd.DataFrame({"col": ["a b c", "d e"]})
    words, chars = calculate_text_metrics(df, [])


def test_nan_and_none_behavior_counts_as_strings():
    # This test documents the actual behavior: the function astypes to str before fillna,
    # which means None and NaN become the literal strings 'None' and 'nan' respectively,
    # and are therefore counted as words and characters.
    df = pd.DataFrame(
        {
            "c": [
                None,            # astype(str) -> 'None' => 1 word, 4 chars
                float("nan"),    # astype(str) -> 'nan'  => 1 word, 3 chars
                "",              # '' -> 0 words, 0 chars
                "nan",           # literal 'nan' -> 1 word, 3 chars
                "None",          # literal 'None' -> 1 word, 4 chars
            ]
        }
    )
    words, chars = calculate_text_metrics(df, ["c"])


def test_duplicate_columns_are_counted_multiple_times():
    # If the same column name appears more than once in text_columns,
    # the implementation iterates and will double-count that column.
    df = pd.DataFrame({"t": ["a b", "c"]})  # words 2 + 1 = 3, chars 3 + 1 = 4
    # Provide the same column twice; expected result should be doubled.
    words, chars = calculate_text_metrics(df, ["t", "t"])


def test_non_string_types_are_converted_to_str_and_counted():
    # Ensure different non-string types are converted to their string representations
    # and then counted for words and characters as the function does via astype(str).
    mixed = [123, 45.6, True, ["list"], {"k": "v"}]
    df = pd.DataFrame({"mixed": mixed})
    words, chars = calculate_text_metrics(df, ["mixed"])
    # Compute expected by applying Python's str conversion to each element, mirroring astype(str)
    expected_words = sum(len(str(x).split()) for x in mixed)
    expected_chars = sum(len(str(x)) for x in mixed)


def test_large_scale_correctness_with_1000_rows():
    # Large-scale test: construct 1000 rows and multiple columns to ensure scalability
    n = 1000
    col1 = [("a" * (i % 10)) for i in range(n)]  # varying lengths 0..9, each non-empty -> 1 word
    # create repeated 'b' tokens with spaces, stripped so split() yields exact counts
    col2 = [(" ".join(["b"] * (i % 5))) for i in range(n)]
    # create a column with up to 6 tokens
    col3 = [(" ".join(["x"] * (i % 7))) for i in range(n)]

    df = pd.DataFrame({"c1": col1, "c2": col2, "c3": col3})

    # Compute expected results by using the same logic as the implementation:
    # convert to str (they already are strings), then use .split() and len()
    expected_words = 0
    expected_chars = 0
    for col in ["c1", "c2", "c3"]:
        series = df[col].astype(str)  # mirrors function behavior
        # sum of character counts
        expected_chars += int(series.str.len().sum())
        # sum of words per row
        expected_words += int(series.str.split().str.len().sum())

    # Use the function and assert results match the expected computation
    words, chars = calculate_text_metrics(df, ["c1", "c2", "c3", "nonexistent_col"])
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import pandas as pd  # real class constructor for DataFrame
# imports
import pytest  # used for our unit tests
from langflow.api.utils.kb_helpers import calculate_text_metrics


def test_basic_single_column_counts_words_and_characters():
    # Create a simple DataFrame with one text column using the real pandas DataFrame constructor
    df = pd.DataFrame({"text": ["hello world", "foo"]})
    # Call the function under test
    words, chars = calculate_text_metrics(df, ["text"])


def test_multiple_columns_and_ignores_missing_columns():
    # DataFrame with two text columns and one numeric column
    df = pd.DataFrame(
        {
            "a": ["one two", "three"],
            "b": ["x y z", ""],
            "num": [1, 2],  # numeric column should not be counted unless listed
        }
    )
    # Include an extra non-existent column name 'missing' which should be skipped silently
    words, chars = calculate_text_metrics(df, ["a", "b", "missing"])


def test_empty_dataframe_and_empty_text_columns_returns_zeros():
    # Empty DataFrame with no columns
    df_empty = pd.DataFrame()
    # When text_columns is empty
    w1, c1 = calculate_text_metrics(df_empty, [])
    # When text_columns contains names not present in the DataFrame: they should be ignored
    w2, c2 = calculate_text_metrics(df_empty, ["nonexistent", "also_missing"])


def test_none_and_nan_and_numeric_values_are_cast_to_strings_and_counted():
    # The implementation calls astype(str) first, so None -> 'None' and nan -> 'nan'
    df = pd.DataFrame({"t": [None, float("nan"), 123]})
    words, chars = calculate_text_metrics(df, ["t"])


def test_whitespace_and_special_characters_counted_correctly():
    # Strings with newlines, tabs, and multiple spaces
    df = pd.DataFrame(
        {
            "s": [
                "a\nb\tc  d",  # a newline and a tab and double space -> 4 words
                "\tleading and trailing \n",  # words: "leading","and","trailing" -> 3 words
            ]
        }
    )
    words, chars = calculate_text_metrics(df, ["s"])
    # Count words explicitly to make the assertion clear and robust
    expected_words = sum(len(str(val).split()) for val in df["s"])
    expected_chars = sum(len(str(val)) for val in df["s"])


def test_large_scale_1000_rows_multiple_columns():
    # Construct 1000 rows to validate scaling up to the requested size
    n_rows = 1000
    # Column a: 5 occurrences of 'word ' (with trailing space); split() will count 5 words
    # Column b: single token of 10 'x' characters -> 1 word per row
    col_a = [("word " * 5) for _ in range(n_rows)]  # each entry length = 5 * len("word ") = 25 chars
    col_b = [("x" * 10) for _ in range(n_rows)]  # each entry length = 10 chars
    df = pd.DataFrame({"a": col_a, "b": col_b})
    words, chars = calculate_text_metrics(df, ["a", "b"])


def test_order_of_text_columns_does_not_change_result():
    # Ensure different ordering of columns in the list yields the same totals
    df = pd.DataFrame({"c1": ["one two"], "c2": ["three four five"]})
    codeflash_output = calculate_text_metrics(df, ["c1", "c2"]); res1 = codeflash_output
    codeflash_output = calculate_text_metrics(df, ["c2", "c1"]); res2 = codeflash_output


def test_mixed_type_columns_handled_and_counted_as_strings():
    df = pd.DataFrame({"m": [100, 200.5, "text"]})
    words, chars = calculate_text_metrics(df, ["m"])
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To test or edit this optimization locally git merge codeflash/optimize-pr11541-2026-02-26T14.38.38

Suggested change
total_characters += int(text_series.str.len().sum())
total_words += int(text_series.str.split().str.len().sum())
# Use a single pass over the string values to avoid multiple pandas string-method allocations.
arr = text_series.to_numpy()
total_characters += int(sum(len(s) for s in arr))
total_words += int(sum(len(s.split()) for s in arr))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed the review comments.

@Cristhianzl

Cristhianzl commented Feb 26, 2026

Copy link
Copy Markdown
Member

Code Review - Remaining Items

IMPORTANT — Must fix before merge

1. Internal errors exposed to end users (knowledge_bases.py)

7 instances of {e!s} leaking internal details (paths, SQLite errors, class names) to the client:

Line 239:  detail=f"Error previewing chunks: {e!s}"
Line 363:  detail=f"Error ingesting files to knowledge base: {e!s}"
Line 460:  detail=f"Error listing knowledge bases: {e!s}"
Line 501:  detail=f"Error getting knowledge base '{kb_name}': {e!s}"
Line 587:  detail=f"Error getting chunks for '{kb_name}': {e!s}"
Line 608:  detail=f"Error deleting knowledge base '{kb_name}': {e!s}"
Line 655:  detail=f"Error deleting knowledge bases: {e!s}"
Line 712:  detail=f"Error cancelling ingestion: {e!s}"

Fix: Log the full error internally, return a generic message:

logger.exception("Error creating knowledge base")
raise HTTPException(status_code=500, detail="Internal error creating knowledge base")

2. Silent failure on column_config parsing (knowledge_bases.py:296-297)

except (json.JSONDecodeError, TypeError):
    pass  # Ignore malformed column_config; use existing schema

Malformed user data is silently swallowed with no logging. This can cause unexpected behavior that is very hard to debug.

Fix: Add at minimum a logger.warning:

except (json.JSONDecodeError, TypeError) as e:
    logger.warning("Malformed column_config received, using existing schema: %s", e)

3. Broad except Exception with noqa: BLE001 (7 occurrences)

knowledge_bases.py (2):

  • Line 82
  • Line 226

kb_helpers.py (5):

  • Lines 69, 93, 147, 181, 447

These suppress the linter instead of handling specific exceptions. Most can be narrowed to OSError, chromadb.errors.ChromaError, json.JSONDecodeError, etc.


RECOMMENDED — Should fix

4. f-strings in logger calls

Multiple files use f-string interpolation in logger calls, which causes unnecessary string allocation even when the log level is disabled:

# Current
logger.warning(f"Initial Chroma setup for {kb_name} failed: {e}")

# Preferred
logger.warning("Initial Chroma setup for %s failed: %s", kb_name, e)

5. Redundant comments

# Helper methods moved to utils/kb_helpers.py appears multiple times — version control already tracks this.


TESTING — Gaps

6. Weak assertion in test_perform_ingestion_rollback

# test_knowledge_bases_api.py line 487
assert mock_build is not None  # This is not a real assertion

This only verifies the mock object exists, not that it was called or returned expected values.

7. No test for malformed column_config

The except (json.JSONDecodeError, TypeError): pass at line 296 has no test coverage. A test sending malformed JSON in column_config should verify the endpoint still succeeds using existing schema.


Summary

Category Count Status
CRITICAL (Blockers) 0 All resolved
IMPORTANT (Must fix) 3 Items 1, 2, 3
RECOMMENDED 2 Items 4, 5
TESTING gaps 2 Items 6, 7

Resolving the 3 IMPORTANT items unblocks approval.

@Cristhianzl Cristhianzl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Comment on lines +291 to +298

class KBIngestionHelper:
"""Helper class for Knowledge Base ingestion processes."""

@staticmethod
async def perform_ingestion(
kb_name: str,
kb_path: Path,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡️Codeflash found 899% (8.99x) speedup for KBAnalysisHelper._get_text_columns in src/backend/base/langflow/api/utils/kb_helpers.py

⏱️ Runtime : 12.6 milliseconds 1.26 milliseconds (best of 99 runs)

📝 Explanation and details

Brief: The optimized version speeds up membership and dtype checks by moving repeated, Python-level work into O(1) set lookups and a single, vectorized pandas call. This removes expensive per-column Python work and uses faster C-level pandas routines where possible — resulting in ~10x runtime improvement (12.6ms -> 1.26ms).

What changed

  • Schema branch: build cols_set = set(df.columns) once and use it for membership checks instead of repeatedly checking col in df.columns.
  • Common-name branch: convert common_names into a set (common_set) so the col.lower() membership test is O(1) instead of scanning a small list each time.
  • Fallback branch: replace the Python loop [col for col in df.columns if df[col].dtype == "object"] with pandas' vectorized df.select_dtypes(include=["object"]).columns and cast to list.

Why these changes are faster

  • set membership is average O(1) vs repeated index/list membership which is O(n) or O(log n) for Index.contains; when schema_data or df.columns is large, switching to sets reduces many lookups from costly operations to cheap ones.
  • The original fallback did a Python-level iteration accessing df[col].dtype for every column. That is relatively expensive due to attribute access and per-Series work. select_dtypes is implemented in pandas/Cython and performs dtype selection much more efficiently in bulk, removing Python per-column overhead.
  • Converting common names to a set eliminates repeated linear scans of the small list, which helps when df has many columns (col.lower() in common_set is O(1) rather than O(k) per column).
  • The line profiler confirms the hot spots moved: the original spent almost all time in the per-column dtype loop; the optimized version pushes that work into select_dtypes and uses sets for membership, dramatically reducing total time.

Behavioral/compatibility notes

  • The functional behavior is preserved: schema-priority logic, case-insensitive common-name detection, and the final fallback (select only object-dtype columns) remain the same. Using select_dtypes(include=["object"]) matches the original intent of selecting columns with dtype == "object".
  • Memory overhead: creating small sets is negligible compared to the performance benefits. If this function is called repeatedly on the same DataFrame in a tight loop, you could micro-optimize further by caching df.columns as a set outside the function.

Which workloads benefit most

  • Large schema_data and/or DataFrames with many columns (see annotated tests test_large_schema_search_performance_and_correctness, test_large_dataframe_object_dtype_returns_all_object_columns, test_large_scale_* cases). These tests show the biggest wins because they trigger many membership/dtype checks.
  • Small DataFrames still benefit but the relative gain is smaller.

Summary

  • Replaced repeated Python-level per-column operations with O(1) set lookups and a vectorized pandas call.
  • This reduces algorithmic cost of the hot paths and leverages optimized pandas internals, producing the observed ~10x speedup while preserving behavior expected by the regression tests.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 12 Passed
🌀 Generated Regression Tests 20 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
⚙️ Click to see Existing Unit Tests
🌀 Click to see Generated Regression Tests
import pandas as pd  # used to construct DataFrame inputs
# imports
import pytest  # used for our unit tests
from langflow.api.utils.kb_helpers import KBAnalysisHelper


def test_schema_prioritized_columns_present():
    # Create a DataFrame with several columns including 'a' which the schema will mark as text
    df = pd.DataFrame({"a": ["hello"], "b": [1], "text": ["x"]})
    # Schema marks 'a' as vectorize=True and data_type='string'; also include a missing column
    schema = [
        {"column_name": "a", "vectorize": True, "data_type": "string"},
        {"column_name": "missing", "vectorize": True, "data_type": "string"},
    ]
    # Expect the helper to return only 'a' because it's the only schema-declared text column present in df
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema); result = codeflash_output


def test_schema_ignored_when_not_vectorize_or_not_string_and_common_name_matches():
    # DataFrame contains a common-name column 'Content' (mixed-case)
    df = pd.DataFrame({"id": [1], "Content": ["hello world"], "other": [2]})
    # Schema exists but does not mark any valid text columns (vectorize=False or wrong data_type). Should fall back.
    schema = [
        {"column_name": "id", "vectorize": False, "data_type": "string"},
        {"column_name": "other", "vectorize": False, "data_type": "integer"},
    ]
    # With no effective schema text columns, the helper should detect 'Content' by case-insensitive common names
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema); result = codeflash_output


def test_common_name_detection_multiple_order_preserved():
    # DataFrame with multiple common-name columns in a particular order and mixed casing
    df = pd.DataFrame({"Text": ["a"], "alpha": [0], "CONTENT": ["b"], "Document": ["c"], "z": [1]})
    # No schema_data provided -> should match common names in the DataFrame column order
    codeflash_output = KBAnalysisHelper._get_text_columns(df); result = codeflash_output


def test_fallback_to_object_dtype_when_no_schema_and_no_common_names():
    # Create DataFrame with one object dtype column ('col1') and one numeric column ('col2')
    df = pd.DataFrame({"col1": ["s1", "s2"], "col2": [1, 2]})
    # No schema and no common-name columns -> should return columns whose dtype is object
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output


def test_empty_dataframe_returns_empty_list():
    # Empty DataFrame with no columns
    df = pd.DataFrame()
    # Neither schema nor columns -> should return empty list
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output


def test_schema_columns_not_in_df_falls_back_to_common_names():
    # Schema points to a column not present in df; df contains 'chunk' so should be detected via common names
    df = pd.DataFrame({"chunk": ["part1"], "other": [1]})
    schema = [{"column_name": "not_present", "vectorize": True, "data_type": "string"}]
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=schema); result = codeflash_output



def test_large_schema_search_performance_and_correctness():
    # Build a large DataFrame with 1000 columns; include a special target column 'col_500' that the schema will mark
    data = {f"col_{i}": [f"v{i}"] for i in range(1000)}
    df = pd.DataFrame(data)
    # Create a large schema: most entries are not vectorized, except one that should be selected
    schema = [
        {"column_name": f"col_{i}", "vectorize": False, "data_type": "string"} for i in range(1000)
    ]
    # Mark a single column as vectorize True and data_type 'string'
    schema[500] = {"column_name": "col_500", "vectorize": True, "data_type": "string"}
    # The helper should return only the single column marked for vectorization and present in df
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=schema); result = codeflash_output


def test_large_dataframe_object_dtype_returns_all_object_columns():
    # Construct a DataFrame with 1000 object-typed columns (strings)
    n = 1000
    data = {f"obj_{i}": [f"v{i}"] for i in range(n)}
    df = pd.DataFrame(data)
    # No schema and no common-name columns; should return all columns because all are object dtype
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import pandas as pd  # DataFrame construction and dtypes
# imports
import pytest  # used for our unit tests
from langflow.api.utils.kb_helpers import KBAnalysisHelper


def test_schema_data_prefers_vectorized_string_columns_basic():
    # Create a DataFrame with some columns present
    df = pd.DataFrame(
        {
            "a": [1],  # numeric column
            "text_col": ["hello"],  # object/string column
            "b": ["x"],  # object/string column
        }
    )
    # schema_data lists three columns; two exist in df, one does not.
    schema_data = [
        {"column_name": "b", "vectorize": True, "data_type": "string"},
        {"column_name": "a", "vectorize": True, "data_type": "string"},
        {"column_name": "missing", "vectorize": True, "data_type": "string"},
    ]
    # Expect the function to return the intersection of schema order with df.columns:
    # order preserved from schema list but only existing columns kept -> ['b','a']
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output


def test_schema_data_ignores_non_vectorized_or_non_string_entries():
    # Construct a DataFrame with two columns
    df = pd.DataFrame({"c": ["one"], "text": ["two"]})
    # schema_data includes a valid entry for 'c' and two invalid entries
    schema_data = [
        {"column_name": "c", "vectorize": True, "data_type": "string"},
        {"column_name": "text", "vectorize": False, "data_type": "string"},  # vectorize False -> ignore
        {"column_name": "text", "vectorize": True, "data_type": "int"},  # not string -> ignore
    ]
    # Only 'c' should be selected from schema_data
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output


def test_schema_data_with_entries_not_in_dataframe_returns_empty_list_no_fallback():
    # When schema_data yields text_columns but none are in df.columns, the function returns []
    df = pd.DataFrame({"Content": ["alpha"], "other": ["beta"]})
    # schema_data contains valid vectorize/string entries but none of these columns exist in df
    schema_data = [
        {"column_name": "not_here", "vectorize": True, "data_type": "string"},
        {"column_name": "also_missing", "vectorize": True, "data_type": "string"},
    ]
    # Because text_columns from schema_data is non-empty, the code will attempt to filter them by df.columns
    # and return the filtered list (which should be empty) instead of falling back to common names.
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output


def test_common_names_detection_case_insensitive_and_preserve_df_order():
    # Create DataFrame with common names in mixed case and specific order
    df = pd.DataFrame(
        {
            "id": [1],
            "Document": ["doc1"],
            "Text": ["txt1"],
            "other": ["o"],
            "chunk": ["c"],  # also a common name (lowercase)
        }
    )
    # No schema_data provided, so common name detection should run.
    # It should be case-insensitive and preserve the order of df.columns.
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output


def test_fallback_to_object_dtype_only_includes_object_series():
    # Column 'b' has dtype object (default for Python strings), 'c' has pandas string dtype
    df = pd.DataFrame(
        {
            "a": [1, 2],  # int -> not object
            "b": ["x", "y"],  # object dtype (default)
            "c": pd.Series(["s1", "s2"], dtype="string"),  # pandas StringDtype, not 'object'
        }
    )
    # No schema_data and no common names -> final fallback should return only columns with dtype == 'object'
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output


def test_empty_dataframe_returns_empty_list():
    # Empty DataFrame with no columns
    df_empty = pd.DataFrame()
    # No schema_data -> should return empty list
    codeflash_output = KBAnalysisHelper._get_text_columns(df_empty, schema_data=None); result = codeflash_output

    # If schema_data is an empty list (falsy), behavior is same as None -> empty DataFrame still returns []
    codeflash_output = KBAnalysisHelper._get_text_columns(df_empty, schema_data=[]); result2 = codeflash_output


def test_schema_data_none_and_empty_list_behave_same_as_no_schema():
    # DataFrame with a common-name column to confirm fallback path works when schema_data is None or empty list
    df = pd.DataFrame({"content": ["a"], "other": ["b"]})
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); res_none = codeflash_output
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=[]); res_empty = codeflash_output


def test_non_string_object_dtype_values_still_count_as_object():
    # Column 'obj' will contain dictionaries; dtype should be 'object' and therefore be selected by fallback
    df = pd.DataFrame({"obj": [{"k": 1}, {"k": 2}], "num": [1, 2]})
    codeflash_output = KBAnalysisHelper._get_text_columns(df); res = codeflash_output


def test_large_scale_schema_and_dataframe_intersection_preserves_schema_order():
    # Build a large DataFrame with 1000 columns named col0..col999
    n = 1000
    data = {}
    for i in range(n):
        # Use small lists for values; dtype will be object because they are Python strings
        data[f"col{i}"] = [f"v{i}"]
    df = pd.DataFrame(data)

    # Build schema_data of length 1000 where every even-indexed column is vectorize True/string
    schema_data = []
    expected = []
    # Put some names not in df as well to ensure filtering works
    for i in range(n):
        entry = {
            "column_name": f"col{i}" if (i % 3 != 0) else f"missing_{i}",  # every 3rd is missing
            "vectorize": (i % 2 == 0),  # even indices vectorize True
            "data_type": "string" if (i % 5 != 0) else "int",  # some are not strings
        }
        schema_data.append(entry)
        # Determine expected: column must be vectorize True, data_type 'string', and exist in df
        if entry["vectorize"] and entry["data_type"] == "string" and entry["column_name"] in df.columns:
            expected.append(entry["column_name"])

    # Call the helper and verify it returns the expected intersection in schema order
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output


def test_large_scale_common_names_detection_in_big_dataframe():
    # Build a large DataFrame with 1000 columns but include a few common-name columns scattered
    n = 1000
    cols = [f"col{i}" for i in range(n)]
    # Insert some common names at specific positions
    cols[10] = "Text"
    cols[200] = "content"
    cols[999] = "Document"
    # Create DataFrame with these columns
    df = pd.DataFrame({c: [f"value_{i}"] for i, c in enumerate(cols)})
    # No schema_data -> should detect the three common-name columns in df order
    codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To test or edit this optimization locally git merge codeflash/optimize-pr11541-2026-02-26T21.08.57

Suggested change
class KBIngestionHelper:
"""Helper class for Knowledge Base ingestion processes."""
@staticmethod
async def perform_ingestion(
kb_name: str,
kb_path: Path,
cols_set = set(df.columns)
return [col for col in text_columns if col in cols_set]
common_names = ["text", "content", "document", "chunk"]
common_set = set(common_names)
text_columns = [col for col in df.columns if col.lower() in common_set]
if text_columns:
return text_columns
return list(df.select_dtypes(include=["object"]).columns)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lgtm This PR has been approved by a maintainer migration Mark if a DB model migration/alembic changes/migration script change is included. size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants